package util.ui;

import java.awt.event.ActionListener;
import java.io.File;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.RandomAccessFile;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Collections;
import java.util.Comparator;
import java.util.Vector;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import jpave.query_interface.FieldMapper;
import util.ui.SortableTable;
import util.Debug.ErrorReport;

/**
 * @author mdb, added 8/21/08
 *
 * A SortableTable that is dynamically linked to a cache file in order 
 * to reduce memory usage (with a small cost to real-time performance).
 */
public class CachedTable extends SortableTable {
	private static final long serialVersionUID = 6393222947327727667L;

	public static final String CACHE_FILE_DELIMITER = "\t";
	
	private static final int CACHE_SIZE = 2000; // number of rows to cache
	private static final int CACHE_FETCH_SIZE = CACHE_SIZE/2;
	
	private static final String NEWLINE = System.getProperty("line.separator");
	private static final int NEWLINE_LENGTH = System.getProperty("line.separator").length();
	
	public CachedTable (FieldMapper inFields, 
						String inCacheFileName, 
						int[] inDefaultFields,
						final ActionListener refreshListener)
	throws Exception
	{
		super(inFields, null, inDefaultFields, refreshListener);
		
		fields = inFields;
		curCacheFileName = strCacheFileName = inCacheFileName;
		curCacheFile = mainCacheFile = new RandomAccessFile( inCacheFileName, "rw" );
		
		nTotalRows = initCache();
		nVisibleRows = nTotalRows;
		curLineToPos = mainLineToPos;
        
		if (nTotalRows == 0)
			return;
		
		if (nTotalRows > 1)
			bAverages = true;

		clearCache();
		readLineAt(0); // pre-load cache for autofitColumns() 
        autofitColumns();
        
	}
	
	private void addRowToAverages(Object[] avg, Object[] row)
	{
		//showRow("adding row", row);
		//showRow("averages  ", avg);
		for (int i = 0;  i < row.length;  i++) {	
			if (row[i] == null || avg[i] == null) {}
			else if (row[i] instanceof DisplayInt) {
				DisplayInt val = (DisplayInt)row[i];
				((DisplayInt)avg[i]).add(val.intValue());
			}
			else if (row[i] instanceof DisplayFloat) {
				DisplayFloat val = (DisplayFloat)row[i];
				((DisplayFloat)avg[i]).add(val.getValue());
			}
			else if (row[i] instanceof Integer) {
				Integer val1 = (Integer)row[i];
				Integer val2 = (Integer)avg[i];
				avg[i] = new Integer(val1.intValue()+val2.intValue());
			}
			else if (row[i] instanceof Long) {
				Long val1 = (Long)row[i];
				Long val2 = (Long)avg[i];
				avg[i] = new Long(val1.longValue()+val2.longValue());
			}
		}
	}
	
	private void finalizeAverages(Object[] tempAverages, int lineCount)
	{
    	if (tempAverages != null) {
	    	for (int i = 0;  i < tempAverages.length;  i++) {
				if (tempAverages[i] instanceof DisplayInt) {
					DisplayInt val = (DisplayInt)tempAverages[i];
					val.div(lineCount);
				}
				else if (tempAverages[i] instanceof DisplayFloat) {
					DisplayFloat val = (DisplayFloat)tempAverages[i];
					val.div(lineCount);
				}
				else if (tempAverages[i] instanceof Integer) {
					Integer val = (Integer)tempAverages[i];
					tempAverages[i] = new Integer(val.intValue()/lineCount);
				}
				else if (tempAverages[i] instanceof Long) {
					Long val = (Long)tempAverages[i];
					tempAverages[i] = new Long(val.longValue()/lineCount);
				}
				else {
					tempAverages[i] = "";
				}
	    		averages[i+1] = tempAverages[i];
	    	}
    	}
	}
	
	// Create line-to-filepos hash and compute averages
    private int initCache() throws Exception
    {
    	mainLineToPos = new HashMap<Integer, Long>();
		averages = new Object[fields.getNumFields()+1];
		averages[0] = new String("Average");
    	int lineCount = 0;
    	
    	Object[] tempAverages = null;
    	
    	BufferedReader f = null;
    	try {
    		f = new BufferedReader( new FileReader( new File(strCacheFileName) ) );
	    	if (f != null) {
				String line;
				long pos = 0;
				
	    		while ((line = f.readLine()) != null) {
	    			// Record file position of line
	    			if (lineCount % CACHE_FETCH_SIZE == 0)
	    				mainLineToPos.put(lineCount/CACHE_FETCH_SIZE, pos);
	    			pos += line.length() + NEWLINE_LENGTH;
	    			lineCount++;
	    			
	    			// Compute column averages
	    			String[] row = line.split(CACHE_FILE_DELIMITER);
	    			if (tempAverages == null)
	    				tempAverages = fields.getObjectsFromDB2(row, lineCount);
	    			else
	    				addRowToAverages(tempAverages, fields.getObjectsFromDB2(row, lineCount));		
	    		}
	    	}
    	}
    	catch (Exception e) {
    		String s = "Internal error: init cache";
    		System.err.println(s);
    		ErrorReport.reportError(e);
    	}
    	finally {
    		if (f != null)
    			f.close();
    	}
    	
    	finalizeAverages(tempAverages, lineCount);
    	
    	return lineCount;
    }
    
    private void clearCache() 
    {
    	cacheLines = new HashMap<Integer, String>(Math.min(CACHE_SIZE, nTotalRows));
    }
    
    public void removeCache() {
    	try {
	    	if (mainCacheFile != null)
	    		mainCacheFile.close();
	    	File f = new File(strCacheFileName);
	    	if (f != null) f.delete();
	    }
    	catch (IOException e) {
    		String s = "Internal error: could not remove cache file: " + strCacheFileName;
    		System.err.println(s);
    		ErrorReport.reportError(e, s);
    	}
    	
    	try {
	    	if (searchCacheFile != null) 
	    		searchCacheFile.close();
	    	File f = new File(strCacheFileName + ".cache");
	    	if (f != null) f.delete();
	    }
    	catch (IOException e) {
    		String s = "Internal error: could not remove cache file: " + strCacheFileName + ".cache";
    		System.err.println(s);
    		ErrorReport.reportError(e, s);
    	}
    }
	
	public Object getValueAt( int rowIndex, int columnIndex ) // override SortableTable
	{
		columnIndex = getMappedColumn(columnIndex, false);
		if (  rowIndex > nTotalRows || nTotalRows == 0)
			return null;
		else if (bAverages && rowIndex >= getVisibleRowCount())
			return averages[columnIndex];
		else {	
			Object obj = null;
			try {
				String line = readLineAt(rowIndex);
				if (line == null)
					return null;
				//Object[] row = line.split(CACHE_FILE_DELIMITER);
				
				if (columnIndex-- == 0)  return rowIndex+1;
				
				// Pass the indicated row's object to the indexer to extract the desired field
		        obj = fields.extractFieldByID ( line, getFieldIdxFromColumnIdx ( columnIndex ) );
		        //obj = fields.getObjectFromDBFast(line, columnIndex );
		        obj = convertNull ( obj );
			}
			catch (Exception e) {
				//Don't output the error... it will repeat for ecery cell in the table
				return null;
			}
			//System.err.println("getValueAt: "+rowIndex+","+columnIndex+" "+obj);
			return obj;
		}
	}
	
	private String readLineAt(int nLinePos) 
	{
		String line = null;
		
    	if (cacheLines != null) {
    		// Cache hit
    		if ((line = (String)cacheLines.get(nLinePos)) != null)
    			return line;
    		
    		// Cache miss
    		try {
    			// Reset cache
    			clearCache();
    			int nBlock = nLinePos/CACHE_FETCH_SIZE;
    			if (!curLineToPos.containsKey(nBlock))
    				return null;
    			long pos = (Long)curLineToPos.get(nBlock);
    			int l = (nLinePos/CACHE_FETCH_SIZE)*CACHE_FETCH_SIZE;
    			//System.err.println("seek: "+l);
    			curCacheFile.seek(pos);
    			
    			// Fetch lines
    			//System.err.println("fetch: "+l+":"+(l+CACHE_SIZE-1));
    			for (int i = 0;  i < CACHE_SIZE;  i++) {
    				if ((line = curCacheFile.readLine()) == null) break;
    				cacheLines.put(l, line);
    				l++;
    			}
    			
    			line = (String)cacheLines.get(nLinePos);
    		}
    		catch (Exception e) { 
    			String s = "Internal error: reading line";
    			ErrorReport.prtReport(e, s);
    		}
    	}
    	
    	return line;
	}
	
	public int getEntryCount() // override SortableTable
	{
		return nTotalRows;
	}
	
	public int getVisibleRowCount() // doesn't include averages row // override SortableTable
	{
		return nVisibleRows;
	}
	
	public int getRowCount() // override SortableTable
	{
		return getVisibleRowCount() + (bAverages ? 1 : 0);
	}
	
	// mdb added 8/25/08
	public Object getRowAt(int nRow) // override SortableTable
	{
		//System.err.println("getRowAt: "+nRow);
		
		if (cacheLines == null || nRow < 0 || nRow >= getEntryCount())
			return null;
		
		//String line = (String)cacheLines.get(nRow);
		String line = readLineAt(nRow);
		Object[] row = line.split(CACHE_FILE_DELIMITER);
		return row;
	}
	
	public int getSelectedRow() // override SortableTable
	{
		int nRow = super.getSelectedRow();
		
		if (bAverages && nRow >= getVisibleRowCount()) // abort if "averages" row
			return -1;

		return nRow;
	}
	
	public int[] getSelectedRows() // override SortableTable
	{
		return super.getSelectedRows();
	}
	
	private void addMatches(String strRegEx, int columnIdx) {
		Pattern p = Pattern.compile(strRegEx, Pattern.CASE_INSENSITIVE);
		Matcher m;
		Boolean isZeroLength = (strRegEx.length() == 0); // for speed, any improvement?
		String s2;
		
		averages = new Object[fields.getNumFields()+1];
		averages[0] = new String("Average");
    	Object[] tempAverages = null;
		
		//long startTime = System.currentTimeMillis(); // for debug
		try {
			// Open a temporary buffered stream, much faster than reading 
			// from random-access mainCacheFile.
			BufferedReader reader = new BufferedReader( new FileReader( new File(strCacheFileName) ) );
			String line;
			long pos = 0;
			while ((line = reader.readLine()) != null) {
				String[] row = line.split(CACHE_FILE_DELIMITER);
				s2 = fields.extractFieldByID ( row, getFieldIdxFromColumnIdx ( columnIdx ) ).toString();
				m = p.matcher(s2);
				if (isZeroLength || m.matches()) {
					pos = searchCacheFile.getFilePointer();
					searchCacheFile.writeBytes(line + NEWLINE);

	    			// Compute column averages
	    			if (tempAverages == null)
	    				tempAverages = fields.getObjectsFromDB2(row, nVisibleRows);
	    			else
	    				addRowToAverages(tempAverages, fields.getObjectsFromDB2(row, nVisibleRows));	
					
					// Build line-to-pos hash on the fly
					if (nVisibleRows % CACHE_FETCH_SIZE == 0)
						searchLineToPos.put(nVisibleRows/CACHE_FETCH_SIZE, pos);
					nVisibleRows++;
				}
			}
			reader.close();
		}
		catch (Exception e) {
			String s = "Internal error: adding matches";
			System.err.println(s);
			ErrorReport.reportError(e, s);
		}
		finalizeAverages(tempAverages, nVisibleRows);
		//System.err.println("addMatches: "+strRegEx+" "+(System.currentTimeMillis()-startTime));
	}
	
	public void filterRows(String strSearch, int columnIdx) // override SortableTable
	{
		filterRows(strSearch, columnIdx, true);
	}
	
	public void filterRows(String strSearch, int columnIdx, boolean overwriteExisting) {
		if (cacheLines == null)
			return;
		
		if (strSearch.length() == 0) {
			setVisibleRows(null); // make all rows visible
			curCacheFile = mainCacheFile;
			curLineToPos = mainLineToPos;
			curCacheFileName = strCacheFileName;
			nVisibleRows = nTotalRows;
			clearCache();
			return;
		}
		
		strSearch = strSearch.replaceAll("\\*", ".*");
		
		try {
			if (searchCacheFile == null)
				searchCacheFile = new RandomAccessFile(curCacheFileName = strCacheFileName + ".cache", "rw");
			if (overwriteExisting) { // erase file
				searchCacheFile.setLength(0); 
				searchCacheFile.seek(0);
				searchLineToPos = new HashMap<Integer, Long>();
				nVisibleRows = 0;
			}
			curCacheFile = searchCacheFile;
			curLineToPos = searchLineToPos;
			clearCache();
		}
		catch (Exception e) {
			String s = "Internal error: filtering rows";
			System.err.println(s);
			ErrorReport.reportError(e, s);
		}
		
		if (strSearch != strFilterCopy) {		
//			addMatches(strSearch, columnIdx); // too time-consuming
//			if (nVisibleRows == 0 // no matches, do looser search
//					&& !strSearch.startsWith(".*") 
//					&& !strSearch.endsWith(".*"))
//			{
				addMatches(".*"+strSearch+".*", columnIdx); // search again with wildcards at each end
//			}
			
			strFilterCopy = new String(strSearch);
		}
		
		revalidate(); repaint();
	}
	
	public void filterRows(String strSearch, int columnIdx1, int columnIdx2) {
		filterRows(strSearch, columnIdx1, true);
		filterRows(strSearch, columnIdx2, false);
	}
	
	protected void sortByColumn ( int nColumn, final boolean bInAscending ) // override SortableTable
	{	
//		long startTime = System.currentTimeMillis(); // for debug
		final int nNewSortField = getFieldIdxFromColumnIdx ( nColumn );
		
        if ( nNewSortField < 0 )
            return;
        
		if ( !fields.isFieldSortableByID( nNewSortField ) )
			return;
		// Save the new sort settings
		ntheSortCol = nColumn;
		
		// see if it's a DE column where we want to sort by abs and then +/-
		String dbName = fields.getDBNameByID(nNewSortField);
		final boolean negSort = (dbName != null ? dbName.startsWith("P_") : false);

		Comparator<Object []> sortRows = new Comparator<Object []> () {
			public int compare ( Object [] row1, Object [] row2 ) 
	    	{
//				Object o1 = convertNull ( fields.extractFieldByID ( row1, nNewSortField ) );
//	            Object o2 = convertNull ( fields.extractFieldByID ( row2, nNewSortField ) );
				Object o1 = convertNull ( ((Object[])row1)[1] );
	            Object o2 = convertNull ( ((Object[])row2)[1] );
	            
	            if (negSort)
	            {
	            	Double d1 = Math.abs(((DisplayFloat)o1).getValue());
	            	Double d2 = Math.abs(((DisplayFloat)o2).getValue());
	            	int nRes = 0;
	            	if (d1 < d2) nRes = -1;
	            	else if (d1 > d2) nRes = 1;
	            	else if (((DisplayFloat)o2).getValue() < 0 && ((DisplayFloat)o1).getValue() > 0) nRes = 1;
	            	else if (((DisplayFloat)o1).getValue() < 0 && ((DisplayFloat)o2).getValue() > 0) nRes = -1;
	            	return  (bInAscending ? nRes : -nRes);
	            }
	    	
/*	            if (negSort && (o1 instanceof Float || o1 instanceof Double) &&
	            		(o2 instanceof Float || o2 instanceof Double))
	            {
	            	Double d1 = (Double)o1; 
	            	Double d2 = (Double)o2; 
	            	if (Math.abs(d1) < Math.abs(d2)) return -1;
	            	else if (Math.abs(d1) > Math.abs(d2)) return 1;
	            	else return d1.compareTo(d2);
	            }*/
	            // Sort nulls to the beginning.  This seems fairly natural.  
	            // I tried at the end and it just didn't feel right.
				int nRes = 0;
	            if ( o1 == o2 ) 		nRes = 0;
	            else if ( o1 == null )	nRes = -1;
	            else if ( o2 == null )	nRes = 1;
	            else {
	                Comparable c1 = (Comparable)o1;
	                Comparable c2 = (Comparable)o2;
	                
	                nRes = c1.compareTo( c2 );
	            }
	            
				// Invert the result if this is descending
	    		if ( !bInAscending )
	    			nRes = -nRes;
	    		
	    		return nRes;
	    	}
		};
		
		// Read entire cached table into temporary memory for sorting
		Vector<Object[]> tempRows = new Vector<Object[]>(nTotalRows);
		String[] tempRows2 = new String[nTotalRows];
		try {
			BufferedReader reader = new BufferedReader( new FileReader( new File(curCacheFileName) ) );
			String line;
			int lineNum = 0;
			while ((line = reader.readLine()) != null) {
//				tempRows.add(new Object[] {lineNum, fields.getObjectFromDBFast(line, nSortCol)});
				tempRows.add(new Object[] {lineNum, fields.extractFieldByID(line, nNewSortField)});
				tempRows2[lineNum] = line;
				lineNum++;
			}
			reader.close();
		}
		catch (Exception e) {
			String s = "Internal error: sorting columns";
			System.err.println(s);
			ErrorReport.reportError(e, s);
		}
		
        // Sort
	    Collections.sort( tempRows, sortRows );
	    
		// Write sorted table back to cache file
	    try {
	    	BufferedWriter writer = new BufferedWriter( new FileWriter( new File(curCacheFileName) ) );
	    	//curCacheFile.setLength(0);
	    	curLineToPos = new HashMap<Integer, Long>();
	    	int nRows = 0;
	    	long pos = 0;
	    	
			for (Object[] row : tempRows) {
				// Write row to file
				String line = tempRows2[(Integer)row[0]];
				writer.write(line, 0, line.length());
				writer.newLine();
				
				// Build line-to-pos hash on the fly
				if (nRows % CACHE_FETCH_SIZE == 0)
					curLineToPos.put(nRows/CACHE_FETCH_SIZE, pos);
				
				pos += line.length() + NEWLINE_LENGTH;
				nRows++;
			}
			writer.close();
		}
		catch (Exception e) {
			String s = "Internal error: sorting columns";
			System.err.println(s);
			ErrorReport.reportError(e);
		}
		tempRows = null;	// redundant, but lets get rid of this ASAP
		tempRows2 = null;	// redundant, but lets get rid of this ASAP
		
		clearCache();
		
        // Notify listeners:
		//Removed 8-20-10 MRW - not necessary to update column headers
        //notifyHeaderChanged ();
        notifyTableRowsChanged ();
        
        // Send event for selection changed
        tableSelectionChanged ();
	}

	private HashMap<Integer, String> cacheLines = null;
	private String strCacheFileName;
	private String curCacheFileName;
	private RandomAccessFile mainCacheFile;
	private RandomAccessFile searchCacheFile = null;
	private RandomAccessFile curCacheFile;
	private HashMap<Integer, Long> mainLineToPos = null;
	private HashMap<Integer, Long> searchLineToPos = null;
	private HashMap<Integer, Long> curLineToPos = null;
	private int nVisibleRows;
	private int nTotalRows;
	private FieldMapper fields;
}
